/******************************************************************************************************
Copyright (C) 2020 Mestrelab Research S.L. All rights reserved.

This file is part of the Mnova scripting toolkit.

Authorized users of MNova Software may use this file freely, but this file is provided AS IS
with NO WARRANTY OF ANY KIND, INCLUDING THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE.
*****************************************************************************************************/

/*globals ByteArray, gc, mainWindow, settings, BinaryStream, Dir, MessageBox, NetworkAccessManager, NetworkRequest, ProgressDialog, File, Python, TextStream*/

/*jslint plusplus: true*/


function WebUtilsQT() {
	"use strict";

	//Instance attributes
	this.silence = false; //It Shows progressDialog only for files whose are considered as big  
	this.timeOutHTTP = 1200; //Time in seconds
	this.tempFile = Dir.temp() + "/tempMnovaRequest.dat"; //Data downloaded from the internet
	this.networkBufferSize = 0; //Unlimited. We use all the bandwidth by default.
	this.writeSize = 1000000; //This only works if the networkBufferSize is bigger than the readBufferSize
	this.sizeBytesToConsiderBigFile = 30000000; //A file is considered big its is size is more than 30MB
	this.networkBufferSizeForBigFiles = 0; //Better performance with unlimited. However if writeSize is too big, we should use 900000 in this parameter in order to reduce the buffer to avoid the problems related to RAM memory
}

WebUtilsQT.prototype.patchRequest = function (aURL, aLoginOrToken, aPassword, aData) {
	"use strict";

	var result = this.executeRequest(aURL, undefined, undefined, aLoginOrToken, aPassword, "patchRequest", undefined, undefined, aData);

	return result;
};

WebUtilsQT.prototype.multiPartRequest = function (aURL, aLoginOrToken, aPassword, aMultiPart) {
	"use strict";

	var result = this.executeRequest(aURL, undefined, undefined, aLoginOrToken, aPassword, "multiPartRequest", undefined, aMultiPart);

	return result;
};

WebUtilsQT.prototype.request = function (aURL, aAction, aLoginOrToken, aPassword, aIsBinaryData) {
	"use strict";

	var result = this.executeRequest(aURL, aAction, undefined, aLoginOrToken, aPassword, "request", aIsBinaryData);

	return result;
};

WebUtilsQT.prototype.jsonRequest = function (aURL, aAction, aLoginOrToken, aPassword, aIsBinaryData) {
	"use strict";

	var result = this.executeRequest(aURL, aAction, undefined, aLoginOrToken, aPassword, "jsonRequest", aIsBinaryData);

	return result;
};

WebUtilsQT.prototype.soapRequest = function (aURL, aAction, aSearch) {
	"use strict";

	var result = this.executeRequest(aURL, aAction, aSearch, undefined, undefined, "soapRequest");

	return result;
};

WebUtilsQT.prototype.writeToFile = function (aPathFile, aData, aCounter) {
	"use strict";

	var file, resultFile;

	//Create new file and put the content in
	file = new File(aPathFile);
	if (aCounter === 0 || !file.exists) {
		if (file.exits) {
			file.remove();
		}
		file.open(File.WriteOnly);
	} else {
		file.open(File.Append);
	}
	resultFile = new BinaryStream(file);
	resultFile.writeBytes(aData);
	file.close();

	//Update instance attributes for managing the download properly
	this.totalWritten = file.size;
	this.counter = 0;
	if (!this.silence && mainWindow.hasGui && this.totalBytes !== undefined && this.totalBytes > 0 && this.progressDialog !== undefined) {
		this.progressDialog.value = ((this.totalWritten / this.totalBytes) * 100);
	}

	return;
};

WebUtilsQT.prototype.readFromNetwork = function (aFinished) {
	"use strict";

	var data, auxData;

	//With QT we have to manage HTTP 301 redirect properly. In python this behaviour was transparent for us
	this.possibleRedirect = this.networkReply.attribute(NetworkRequest.RedirectionTargetAttribute);

	//We try to avoid a redirect security issue
	if (this.possibleRedirect !== undefined && this.possibleRedirect !== this.lastRedirect) {

		//Disconnect all signals and set flag finished to false in order to synchronize with main thread
		this.networkReply.downloadProgress.disconnect(this, this.manageDownloadProgress);
		this.networkReply.finished.disconnect(this, this.manageEndingConnection);
		this.networkReply.readyRead.disconnect(this, this.readFromNetwork);
		this.networkReplyFinished = false;
		this.possibleRedirect = this.possibleRedirect.toString();
		new File(this.tempFile).remove();

		//Get new URL taking into account if the redirect points to a HTTP or HTTPS site.
		if (this.possibleRedirect.toLowerCase().indexOf("http://") === -1 && this.possibleRedirect.toLowerCase().indexOf("https://") === -1) {
			if (this.originalURL.toLowerCase().indexOf("http://") !== -1) {
				this.possibleRedirect = "http://" + this.possibleRedirect.substring(2, this.possibleRedirect.length);
			} else {
				this.possibleRedirect = "https://" + this.possibleRedirect.substring(2, this.possibleRedirect.length);
			}
		}
		this.networkRequest.setUrl(this.possibleRedirect);
		this.lastRedirect = this.possibleRedirect;

		//Launch new request with the new URL
		if (this.action === undefined || this.action === "") {
			this.networkReply = this.networkAccessManager.get(this.networkRequest);
		} else {
			this.networkReply = this.networkAccessManager.post(this.networkRequest, this.action.toUtf8());
		}

		//Reset attributes and connect signals again
		this.counter = 0;
		this.totalReads = 0;
		this.networkReply.downloadProgress.connect(this, this.manageDownloadProgress);
		this.networkReply.finished.connect(this, this.manageEndingConnection);
		this.networkReply.readyRead.connect(this, this.readFromNetwork);

		return;
	}

	if (aFinished === undefined || !aFinished) {
		this.networkReply.readyRead.disconnect(this, this.readFromNetwork);
	}

	//Read available data to disk in order to avoid memory problems with very big files
	//For most types of binary, like images in PNG format, pdf Files by using a TextStream is enough.
	//However for certain files, like videos in AVI format or others, we have to use a BinaryStream
	if (this.networkReply.bytesAvailable > 0) {
		data = new BinaryStream(this.networkReply);
		while (this.networkReply.bytesAvailable > 0) {
			//Get data from HTTP network request
			if (this.networkReply.bytesAvailable <= this.writeSize) {
				auxData = data.readBytes(this.networkReply.bytesAvailable);
				this.writeToFile(this.tempFile, auxData, this.totalReads);
			} else {
				while (!data.atEnd()) {
					auxData = [];
					auxData = data.readBytes(this.networkReply.bytesAvailable);
					this.writeToFile(this.tempFile, auxData, this.writeSize);
				}
			}
		}
	}
	this.counter = 0;
	this.networkReplyFinished = this.networkReply.isFinished;
	this.totalReads++;
	if (aFinished === undefined || !aFinished) {
		this.networkReply.readyRead.connect(this, this.readFromNetwork);
	}

	return;
};

WebUtilsQT.prototype.manageEndingConnection = function () {
	"use strict";

	//Get last data from network
	this.errorCode = this.networkReply.networkError;
	this.errorString = this.networkReply.errorString;
	this.readFromNetwork(true);

	return;
};

WebUtilsQT.prototype.manageDownloadProgress = function (aBytesReceived, aBytesTotal) {
	"use strict";

	if (this.totalBytes === 0 && aBytesTotal > 0) {
		this.totalBytes = aBytesTotal;
		if (aBytesTotal > this.sizeBytesToConsiderBigFile) {
			this.networkReply.setReadBufferSize(this.networkBufferSizeForBigFiles);
		} else if (this.networkReply.readBufferSize !== this.networkBufferSize) {
			this.networkReply.setReadBufferSize(this.networkBufferSize);
		}
	}
	//Show information for big files
	if (!this.silence && mainWindow.hasGui && this.progressDialog === undefined && this.totalBytes !== undefined && this.totalBytes >= this.sizeBytesToConsiderBigFile && aBytesReceived > 0) {
		this.progressDialog = new ProgressDialog();
		this.progressDialog.labelText = "Getting data from network ...";
		this.progressDialog.visible = true;
		this.progressDialog.minimum = 0;
	}
	this.counter = 0;

	return;
};

WebUtilsQT.prototype.dataIsAFile = function (aAction) {
	"use strict";

	var result = false;

	if (aAction !== undefined) {
		if (typeof (aAction) === "object") {
			if (aAction.exists !== undefined) {
				if (aAction.exists === true) {
					if (aAction.size !== undefined) {
						if (!isNaN(aAction.size)) {
							if (aAction.size >= 0) {
								result = true;
							}
						}
					}
				}
			}
		}
	}

	return result;
};

WebUtilsQT.prototype.executeRequest = function (aURL, aAction, aSearch, aLoginOrToken, aPassword, aTypeRequest, aIsBinaryData, aMultiPart, aData) {
	"use strict";

	var userAndPass, encodedString, authHeader, file, lastBytesReceived,
		response, isAFile, actionIsAFileToBeSent;

	try {
		//Initialize attributes and vars
		this.counter = 0;
		this.totalReads = 0;
		this.totalBytes = 0;
		this.totalWritten = 0;
		this.networkReplyFinished = false;
		this.action = aAction;
		this.originalURL = aURL;
		lastBytesReceived = 0;
		isAFile = undefined;
		response = "";

		//Create the networkAccessManager object
		this.networkAccessManager = new NetworkAccessManager();

		//Create the networkRequest Object
		this.networkRequest = new NetworkRequest();
		this.networkRequest.setUrl(aURL);

		switch (aTypeRequest) {
		case "patchRequest":

			authHeader = "";

			if (aLoginOrToken !== undefined && aPassword === undefined) {
				authHeader = aLoginOrToken;
			} else if (aLoginOrToken) {
				userAndPass = aLoginOrToken + ":" + aPassword;
				encodedString = userAndPass.toBase64();
				authHeader = "Basic " + encodedString.toUtf8();
			}

			if (authHeader !== "") {
				this.networkRequest.setRawHeader('Authorization', authHeader);
			}
			this.networkReply = this.networkAccessManager.sendCustomRequest(this.networkRequest, "PATCH", aData);

			break;

		case "multiPartRequest":

			authHeader = "";

			if (aLoginOrToken !== undefined && aPassword === undefined) {
				authHeader = aLoginOrToken;
			} else if (aLoginOrToken) {
				userAndPass = aLoginOrToken + ":" + aPassword;
				encodedString = userAndPass.toBase64();
				authHeader = "Basic " + encodedString.toUtf8();
			}

			if (authHeader !== "") {
				this.networkRequest.setRawHeader('Authorization', authHeader);
			}
			this.networkReply = this.networkAccessManager.post(this.networkRequest, aMultiPart);

			break;

		case "request":

			//Manage authorization header
			authHeader = "";

			//HTTP Bearer Authentication 
			//If we have a login but no password, then we assume a token-based authentication type, i.e. bearer authtication
			//Here, the aLoginOrToken parameter contains the bearer token
			if (aLoginOrToken !== undefined && aPassword === undefined) {
				authHeader = aLoginOrToken;

			//HTTP Basic Authentication
			//Otherwise, we use the standard HTTP Basic Authentication
			//Login and password are enconded to Base 64
			} else if (aLoginOrToken) {
				userAndPass = aLoginOrToken + ":" + aPassword;
				encodedString = userAndPass.toBase64();
				authHeader = "Basic " + encodedString.toUtf8();
			}
			this.networkRequest.setRawHeader('Authorization', authHeader);

			//Choose the proper method by analyzing the aAction parameter
			if (aAction === undefined || aAction === "") {
				this.networkReply = this.networkAccessManager.get(this.networkRequest);
			} else {

				//Check action type which should be either a object File or a string
				actionIsAFileToBeSent = this.dataIsAFile(aAction);

				//If the action is a string no changes will be required from the original implementation
				if (!actionIsAFileToBeSent) {
					this.networkReply = this.networkAccessManager.post(this.networkRequest, aAction.toUtf8());

				//Header is multipart because we will send the file as File instead of a binary array
				} else {
					this.networkRequest.setRawHeader('Content-Type', 'multipart/form-data');
					this.networkReply = this.networkAccessManager.post(this.networkRequest, aAction);
				}
			}

			break;

		case "jsonRequest":

			//Manage authorization header
			authHeader = "";

			//HTTP Bearer Authentication 
			//If we have a login but no password, then we assume a token-based authentication type, i.e. bearer authtication
			//Here, the aLoginOrToken parameter contains the bearer token
			if (aLoginOrToken !== undefined && aPassword === undefined) {
				authHeader = aLoginOrToken;

			//HTTP Basic Authentication
			//Otherwise, we use the standard HTTP Basic Authentication
			//Login and password are enconded to Base 64
			} else if (aLoginOrToken) {
				userAndPass = aLoginOrToken + ":" + aPassword;
				encodedString = userAndPass.toBase64();
				authHeader = "Basic " + encodedString.toUtf8();
			}
			this.networkRequest.setRawHeader('Authorization', authHeader);

			//Manage header
			this.networkRequest.setRawHeader('Content-Type', 'application/json');

			//Choose the proper method by analyzing the aAction parameter
			if (aAction === undefined || aAction === "") {
				this.networkReply = this.networkAccessManager.get(this.networkRequest);
			} else {
				this.networkReply = this.networkAccessManager.post(this.networkRequest, aAction.toUtf8());
			}

			break;

		case "soapRequest":

			//Soap request headers
			this.networkRequest.setRawHeader('Content-Type', 'application/soap+xml');
			this.networkRequest.setRawHeader('SOAPAction', aAction);

			//Launch the request
			this.networkReply = this.networkAccessManager.post(this.networkRequest, aSearch.toUtf8());

			break;
		}

		//*************************SIGNALS MANAGEMENT*****************************
		this.networkReply.readyRead.connect(this, this.readFromNetwork);

		//Common code for both request.
		//Reset the time out counter if we are downloading data properly.
		//It is quite important in order to download big data which downlading time be greather than the time out
		if (this.networkReply !== undefined && !this.networkReplyFinished) {
			this.networkReply.downloadProgress.connect(this, this.manageDownloadProgress);
		}

		//Call to the POST or GET HTTP Method and manage the reply
		this.networkReply.finished.connect(this, this.manageEndingConnection);

		//***********************************MAIN CODE SYNCHRONIZED WITH SIGNALS ***************************************
		//Synchroinze the signal with the function by ussing this pattern. Each iteration is a 0.1 second. Maximum wait are defined to 120 second
		while (!this.networkReplyFinished && this.counter < this.timeOutHTTP) {
			mainWindow.sleep(100);
			mainWindow.processEvents();
			this.counter++;
			//Disconnect all signals and finish connection if the user has pulsed the cancel button for big files
			if (this.progressDialog !== undefined && this.progressDialog.wasCanceled) {
				this.networkReply.readyRead.disconnect(this, this.readFromNetwork);
				this.networkReply.downloadProgress.disconnect(this, this.manageDownloadProgress);
				this.networkReply.finished.disconnect(this, this.manageEndingConnection);
				this.progressDialog.visible = false;
				return;
			}
		}

		//Disconnect signal and hide the progressDialog 
		this.networkReply.downloadProgress.connect(this, this.manageDownloadProgress);
		if (this.progressDialog !== undefined) {
			this.progressDialog.visible = false;
		}

		//Set to blank the error description if we haven't got an error.
		if (this.errorCode === 0) {
			this.errorString = "";
		}

		//Get the output by using the proper format
		file = new File(this.tempFile);
		file.open(File.ReadOnly);
		if (aIsBinaryData !== undefined && aIsBinaryData) {
			if (this.totalBytes !== undefined && this.totalBytes >= this.sizeBytesToConsiderBigFile) {
				isAFile = file.absDirPath;
			} else {
				response = new BinaryStream(file);
				response = response.readBytes(file.size);
			}
		} else {
			if (this.totalBytes !== undefined && this.totalBytes >= this.sizeBytesToConsiderBigFile) {
				isAFile = file.absDirPath;
			} else {
				response = new TextStream(file);
				response = response.readAll();
			}
		}
		file.close();
	} catch (err) {
		MessageBox.critical(err);
	} finally {
		//Delete network reply
		this.networkReply.markForDeletion();
		gc();

		//Check for time out error due to a network problem. Return code errors.
		if (!this.networkReplyFinished && this.counter >= this.timeOutHTTP) {
			this.errorString = "Time out error due to a network connection issue.";
			if (this.errorCode === 0) {
				this.errorCode = 1;
			}
		}

		//The allStrOutput is always undefined because with the previous version was also it.
		return {exitCode: this.errorCode, allErrorOutput: this.errorString, allStrOutput: undefined, response: response, isAFile: isAFile, url: this.networkRequest.url};
	}
};
